package org.chipu.jnids.beans;

import static org.chipu.jnids.sce.profile.StatType.Type.*;
import static org.chipu.shared.util.singletons.DebugUtils.getInMultipleLines;
import java.util.*;
import javax.annotation.Nonnull;
import org.chipu.jnids.sce.profile.*;

public final class Profile {
	/** Specifies if it is a white profile or a black profile */
	private final boolean isWhite;
	@Nonnull
	private final String name;
	/** The short = day_of_week<<5 + hour_of_day (64..248) */
	private final HashMap<StatType, Map<Short, CombinedStats>> connectionStats = new HashMap<StatType, Map<Short, CombinedStats>>(
		11);// 8/0.75+1=11
	private int nconnections;
	private transient static Calendar cal;

	public Profile(@Nonnull String name, boolean isWhite) {
		if (name == null)
			throw new IllegalArgumentException();
		this.name = name;
		this.isWhite = isWhite;
	}

	public void addStats(PuntualStats s) {
		Short id = getTimeBasedId(s.timeUTC);
		for(Map.Entry<StatType, Stats> e : s)
			addStats(e.getKey(), e.getValue(), id);
	}

	private static short getTimeBasedId(long time) {
		if (cal == null)
			cal = Calendar.getInstance();
		cal.setTimeInMillis(time);
		return (short) ((cal.get(Calendar.DAY_OF_WEEK) << 5) + cal.get(Calendar.HOUR_OF_DAY));
	}

	/** 1(SUNDAY)..7(SATURDAY) */
	public static byte getDayOfWeekForId(short id) {
		int s = id >>> 5 & 0x7;
		if (s > 7)
			throw new IllegalArgumentException("Not valid id (" + s + "): " + id);
		return (byte) s;
	}

	/** 0..23 */
	public static byte getHourOfDayForId(short id) {
		int s = id & 0x1F;
		if (s > 23)
			throw new IllegalArgumentException("Not valid id (" + s + "): " + id);
		return (byte) s;
	}

	/** Store the statistics in a map by StatType/day_of_the_week-hour_of_the_day combination */
	private void addStats(final StatType type, final Stats s, Short id) {
		nconnections += s.dataCount;
		if (s.isEmpty())
			return;
		Map<Short, CombinedStats> m = connectionStats.get(type);
		if (m == null) {
			// create the map for that StatType
			m = new HashMap<Short, CombinedStats>();
			connectionStats.put(type, m);
		}
		CombinedStats cs = m.get(id);
		if (cs == null)
			m.put(id, new CombinedStats(s));
		else
			cs.add(s);
	}

	// -----

	/** This is called in rule engine (depends on the current time) */
	public Stats getRelatedStats(StatType st) {
		return connectionStats.get(st).get(getTimeBasedId(System.currentTimeMillis())).getStats();
	}

	/** This is called in rule engine (depends on the current time) */

	public boolean isMostActiveHost(StatType st) {
		assert st.type == CONNECTIONS_FROM_EXTERNAL || st.type == CONNECTIONS_FROM_INTERNAL
			|| st.type == CONNECTIONS_TO_EXTERNAL || st.type == CONNECTIONS_TO_INTERNAL;
		Map<Short, CombinedStats> tmp = connectionStats.get(st);
		if (tmp == null)
			return false;
		CombinedStats cs = connectionStats.get(st).get(getTimeBasedId(System.currentTimeMillis()));
		return cs != null;
	}

	public Collection<StatType> getStatTypes() {
		return connectionStats.keySet();
	}

	public Map<Short, CombinedStats> getStats(StatType st) {
		return connectionStats.get(st);
	}

	// -----

	public synchronized String encode() {
		// sort
		LinkedHashMap<StatType, Map<Short, CombinedStats>> map = new LinkedHashMap<StatType, Map<Short, CombinedStats>>(
			connectionStats.size());
		ArrayList<StatType> l1 = new ArrayList<StatType>(connectionStats.keySet());
		Collections.sort(l1);
		for(StatType type : l1) {
			Map<Short, CombinedStats> map2 = connectionStats.get(type);
			Map<Short, CombinedStats> map3 = new LinkedHashMap<Short, CombinedStats>(map2.size());
			ArrayList<Short> l2 = new ArrayList<Short>(map2.keySet());
			Collections.sort(l2);
			for(Short s : l2)
				map3.put(s, map2.get(s));
			map.put(type, map3);
		}

		// create string
		StringBuilder s = new StringBuilder("<profile name=\"").append(name).append("\" white=\"").append(
			isWhite).append("\" nconnections=\"").append(nconnections).append("\">");
		for(Map.Entry<StatType, Map<Short, CombinedStats>> e : map.entrySet()) {
			s.append("<entry>").append(e.getKey().encode());
			for(Map.Entry<Short, CombinedStats> ee : e.getValue().entrySet())
				s.append("<s time=\"").append(ee.getKey()).append("\">").append(ee.getValue().encode())
					.append("</s>");
			s.append("</entry>");
		}
		s.append("</profile>");
		return s.toString();
	}

	public static Profile decode(String s) throws Exception {
		assert s.startsWith("<profile ");
		Profile rta = new Profile(StatType.getValue(s, "name"), Boolean.parseBoolean(StatType.getValue(s,
			"white")));
		int i = 0;
		int entryLength = "<entry>".length();
		int sLength = "<s time=".length() + 1;
		int combinedStatLength = "<combinedstat>".length();
		while(true) {
			i = s.indexOf("<entry>", i);
			if (i == -1)
				break;
			int ii = s.indexOf("</entry>", i);
			if (ii == -1 || ii <= i)
				throw new IllegalArgumentException("Wrong xml profile: " + s);
			String ss = s.substring(i + entryLength, ii).trim();
			StatType type = StatType.decode(ss.substring(0, ss.indexOf("/>") + 2));
			HashMap<Short, CombinedStats> m = new HashMap<Short, CombinedStats>();
			int j = 0;
			while(true) {
				j = ss.indexOf("<s time=", j);
				if (j == -1)
					break;
				short time = Short.parseShort(ss.substring(j + sLength, ss.indexOf("\"", j + sLength + 1)));
				j = ss.indexOf(">", j);
				int jj = ss.indexOf("</s>", j);
				if (jj == -1 || jj <= j)
					throw new IllegalArgumentException("Wrong xml profile: " + ss);
				String tt = ss.substring(j + 1, jj).trim();
				m.put(time, CombinedStats.decode(tt.substring(tt.indexOf("<combinedstat"), tt
					.indexOf("</combinedstat>")
					+ combinedStatLength)));
				j = jj;
			}
			rta.connectionStats.put(type, m);
			i = ii;
		}
		rta.nconnections = StatType.getIntValue(s, "nconnections");
		return rta;
	}

	// -----

	public String getName() {
		return name;
	}

	public boolean isValidProfile() {
		return isWhite;
	}

	@Override
	public String toString() {
		return "Profile: " + name + " (" + (isWhite? "white": "black") + ")\n\tNumber of connections: "
			+ nconnections + "\n\tStats: " + getInMultipleLines(connectionStats, "\t\t").substring(1);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null || !obj.getClass().isAssignableFrom(getClass()))
			return false;
		Profile o = (Profile) obj;
		return name.equals(o.name) && isWhite == o.isWhite && nconnections == o.nconnections
			&& connectionStats.equals(o.connectionStats);
	}
}